package rexsee.security;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import rexsee.core.application.RexseeApplication;
import rexsee.core.browser.Browser;
import rexsee.core.browser.clazz.JavascriptInterface;
import rexsee.core.browser.clazz.SyncTask;
import rexsee.core.browser.clazz.SyncTask.BooleanRunnable;
import rexsee.core.utilities.Utilities;

public class RexseeEncryption implements JavascriptInterface {

	public static final String INTERFACE_NAME = "Encryption";
	@Override
	public String getInterfaceName() {
		return mBrowser.application.resources.prefix + INTERFACE_NAME;
	}
	@Override
	public JavascriptInterface getInheritInterface(Browser childBrowser) {
		return this;
	}
	@Override
	public JavascriptInterface getNewInterface(Browser childBrowser) {
		return new RexseeEncryption(childBrowser);
	}

	protected final Browser mBrowser;

	public RexseeEncryption(Browser browser) {
		mBrowser = browser;
	}

	private static PrivateKey string2privateKey_(String algorithm, String key) {
		try {
			return KeyFactory.getInstance(algorithm).generatePrivate(new PKCS8EncodedKeySpec(RexseeSecurity.decodeKey(key)));
		} catch (Exception e) {
			return null;
		}
	}
	private static PublicKey string2publicKey_(String algorithm, String key) {
		try {
			return KeyFactory.getInstance(algorithm).generatePublic(new X509EncodedKeySpec(RexseeSecurity.decodeKey(key)));
		} catch (Exception e) {
			return null;
		}
	}

	private byte[] des_(int mode, byte[] data, String key) {
		if (data == null || data.length == 0 || key == null || key.equals("")) return null;
		try {
			DESKeySpec dks = new DESKeySpec(RexseeSecurity.decodeKey(key));
			SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(RexseeSecurity.ALGORITHM_DES);
			Key theKey = keyFactory.generateSecret(dks);
			Cipher cipher = Cipher.getInstance(RexseeSecurity.ALGORITHM_DES);
			cipher.init(mode, theKey);
			return cipher.doFinal(data);
		} catch (Exception e) {
			return null;
		}
	}
	private boolean desFile_(final int mode, final String sourcePath, final String targetPath, final String key, boolean showProgress, boolean cancelable) {
		if (!isValidPath(sourcePath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		if (!isValidPath(targetPath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		BooleanRunnable runnable = new BooleanRunnable() {
			@Override
			public boolean run() {
				try {
					byte[] rtn = des_(mode, Utilities.getContent(sourcePath), key);
					return Utilities.putContent(targetPath, rtn);
				} catch (Exception e) {
					return false;
				}
			}
		};
		if (showProgress) {
			return new SyncTask().run(runnable, mBrowser, cancelable);
		} else {
			return new SyncTask().run(runnable, null, false);
		}
	}
	private byte[] des3_(int mode, byte[] data, String key) {
		if (data == null || data.length == 0 || key == null || key.equals("")) return null;
		try {
			DESedeKeySpec dks = new DESedeKeySpec(RexseeSecurity.decodeKey(key));
			SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(RexseeSecurity.ALGORITHM_DES3);
			Key theKey = keyFactory.generateSecret(dks);
			Cipher cipher = Cipher.getInstance(RexseeSecurity.ALGORITHM_DES3);
			cipher.init(mode, theKey);
			return cipher.doFinal(data);
		} catch (Exception e) {
			return null;
		}
	}
	private boolean des3File_(final int mode, final String sourcePath, final String targetPath, final String key, boolean showProgress, boolean cancelable) {
		if (!isValidPath(sourcePath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		if (!isValidPath(targetPath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		BooleanRunnable runnable = new BooleanRunnable() {
			@Override
			public boolean run() {
				try {
					byte[] rtn = des3_(mode, Utilities.getContent(sourcePath), key);
					return Utilities.putContent(targetPath, rtn);
				} catch (Exception e) {
					return false;
				}
			}
		};
		if (showProgress) {
			return new SyncTask().run(runnable, mBrowser, cancelable);
		} else {
			return new SyncTask().run(runnable, null, false);
		}
	}
	private byte[] aes_(int mode, byte[] data, String key) {
		if (data == null || data.length == 0 || key == null || key.equals("")) return null;
		try {
			Key theKey = new SecretKeySpec(RexseeSecurity.decodeKey(key), RexseeSecurity.ALGORITHM_AES);
			Cipher cipher = Cipher.getInstance(RexseeSecurity.ALGORITHM_AES);
			cipher.init(mode, theKey);
			return cipher.doFinal(data);
		} catch (Exception e) {
			return null;
		}
	}
	private boolean aesFile_(final int mode, final String sourcePath, final String targetPath, final String key, boolean showProgress, boolean cancelable) {
		if (!isValidPath(sourcePath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		if (!isValidPath(targetPath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		BooleanRunnable runnable = new BooleanRunnable() {
			@Override
			public boolean run() {
				try {
					byte[] rtn = aes_(mode, Utilities.getContent(sourcePath), key);
					return Utilities.putContent(targetPath, rtn);
				} catch (Exception e) {
					return false;
				}
			}
		};
		if (showProgress) {
			return new SyncTask().run(runnable, mBrowser, cancelable);
		} else {
			return new SyncTask().run(runnable, null, false);
		}
	}
	private byte[] pbe_(String algorithm, int mode, byte[] data, String password, byte[] salt) {
		if (data == null || data.length == 0 || password == null || password.equals("")) return null;
		try {
			PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
			SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
			Key theKey = keyFactory.generateSecret(keySpec);
			PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 100);
			Cipher cipher = Cipher.getInstance(algorithm);
			cipher.init(mode, theKey, paramSpec);
			return cipher.doFinal(data);
		} catch (Exception e) {
			return null;
		}
	}
	private boolean pbeFile_(final String algorithm, final int mode, final String sourcePath, final String targetPath, final String key, final String salt, boolean showProgress, boolean cancelable) {
		if (!isValidPath(sourcePath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		if (!isValidPath(targetPath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		BooleanRunnable runnable = new BooleanRunnable() {
			@Override
			public boolean run() {
				try {
					byte[] rtn = pbe_(algorithm, mode, Utilities.getContent(sourcePath), key, RexseeSecurity.hex2bytes(salt));
					return Utilities.putContent(targetPath, rtn);
				} catch (Exception e) {
					return false;
				}
			}
		};
		if (showProgress) {
			return new SyncTask().run(runnable, mBrowser, cancelable);
		} else {
			return new SyncTask().run(runnable, null, false);
		}
	}

	public String generateKey_(String algorithm, String seed, int keySize) {
		try {
			SecureRandom secureRandom = (seed == null || seed.equals("")) ? new SecureRandom() : new SecureRandom(seed.getBytes());
			KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
			keyGenerator.init(keySize, secureRandom);
			SecretKey secretKey = keyGenerator.generateKey();
			return RexseeSecurity.key2string_(secretKey);
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + e.getLocalizedMessage();
		}
	}

	//JavaScript interface

	protected boolean isValidPath(String path) {
		return true;
	}

	//DES
	public String generateDESKey(String seed) {
		return generateKey_(RexseeSecurity.ALGORITHM_DES, seed, 64);
	}
	public String encryptWithDES(String data, String key) {
		try {
			byte[] rtn = des_(Cipher.ENCRYPT_MODE, data.getBytes(mBrowser.application.getDefaultEncoding()), key);
			return RexseeSecurity.bytes2hex(rtn);
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + e.getLocalizedMessage();
		}
	}
	public boolean encryptWithDES(String sourcePath, String targetPath, String key, boolean showProgress, boolean cancelable) {
		return desFile_(Cipher.ENCRYPT_MODE, sourcePath, targetPath, key, showProgress, cancelable);
	}
	public String decryptWithDES(String data, String key) {
		try {
			byte[] rtn = des_(Cipher.DECRYPT_MODE, RexseeSecurity.hex2bytes(data), key);
			return new String(rtn, mBrowser.application.getDefaultEncoding());
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + e.getLocalizedMessage();
		}
	}
	public boolean decryptWithDES(String sourcePath, String targetPath, String key, boolean showProgress, boolean cancelable) {
		return desFile_(Cipher.DECRYPT_MODE, sourcePath, targetPath, key, showProgress, cancelable);
	}

	//DES3
	public String generateDES3Key(String seed) {
		return generateKey_(RexseeSecurity.ALGORITHM_DES3, seed, 192);
	}
	public String encryptWithDES3(String data, String key) {
		try {
			byte[] rtn = des3_(Cipher.ENCRYPT_MODE, data.getBytes(mBrowser.application.getDefaultEncoding()), key);
			return RexseeSecurity.bytes2hex(rtn);
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + e.getLocalizedMessage();
		}
	}
	public boolean encryptWithDES3(String sourcePath, String targetPath, String key, boolean showProgress, boolean cancelable) {
		return des3File_(Cipher.ENCRYPT_MODE, sourcePath, targetPath, key, showProgress, cancelable);
	}
	public String decryptWithDES3(String data, String key) {
		try {
			byte[] rtn = des3_(Cipher.DECRYPT_MODE, RexseeSecurity.hex2bytes(data), key);
			return new String(rtn, mBrowser.application.getDefaultEncoding());
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + e.getLocalizedMessage();
		}
	}
	public boolean decryptWithDES3(String sourcePath, String targetPath, String key, boolean showProgress, boolean cancelable) {
		return des3File_(Cipher.DECRYPT_MODE, sourcePath, targetPath, key, showProgress, cancelable);
	}

	//AES
	public String generateAESKey(String seed, int keySize) {
		if (keySize != 128 && keySize != 192 && keySize != 256) return RexseeApplication.EXCEPTION_PREFIX + "Invalid key size. AES key size must be 128, 192 or 256.";
		return generateKey_(RexseeSecurity.ALGORITHM_AES, seed, keySize);
	}
	public String encryptWithAES(String data, String key) {
		try {
			byte[] rtn = aes_(Cipher.ENCRYPT_MODE, data.getBytes(mBrowser.application.getDefaultEncoding()), key);
			return RexseeSecurity.bytes2hex(rtn);
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + e.getLocalizedMessage();
		}
	}
	public boolean encryptWithAES(String sourcePath, String targetPath, String key, boolean showProgress, boolean cancelable) {
		return aesFile_(Cipher.ENCRYPT_MODE, sourcePath, targetPath, key, showProgress, cancelable);
	}
	public String decryptWithAES(String data, String key) {
		try {
			byte[] rtn = aes_(Cipher.DECRYPT_MODE, RexseeSecurity.hex2bytes(data), key);
			return new String(rtn, mBrowser.application.getDefaultEncoding());
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + e.getLocalizedMessage();
		}
	}
	public boolean decryptWithAES(String sourcePath, String targetPath, String key, boolean showProgress, boolean cancelable) {
		return aesFile_(Cipher.DECRYPT_MODE, sourcePath, targetPath, key, showProgress, cancelable);
	}

	//RSA

	public String encryptWithRSA(String dataString, String key, boolean isPublicKey) {
		if (dataString == null || dataString.length() == 0) return RexseeApplication.EXCEPTION_PREFIX + "Data Error.";
		if (key == null || key.equals("")) return RexseeApplication.EXCEPTION_PREFIX + "Key Error.";
		try {
			byte[] data = dataString.getBytes(mBrowser.application.getDefaultEncoding());
			Key theKey = (isPublicKey) ? string2publicKey_(RexseeSecurity.ALGORITHM_RSA, key) : string2privateKey_(RexseeSecurity.ALGORITHM_RSA, key);
			Cipher cipher = Cipher.getInstance(RexseeSecurity.ALGORITHM_RSA);//RSA/ECB/PKCS1Padding
			cipher.init(Cipher.ENCRYPT_MODE, theKey);
			byte[] rtn;
			int blockSize = cipher.getBlockSize();
			int outputBlockSize = blockSize + 1;
			int blocks = data.length / blockSize;
			int lastBlockSize = data.length % blockSize;
			rtn = new byte[(lastBlockSize == 0 ? blocks : blocks + 1) * outputBlockSize];
			for (int i = 0; i < blocks; i++) {
				cipher.doFinal(data, i * blockSize, blockSize, rtn, i * outputBlockSize);
			}
			if (lastBlockSize != 0) {
				cipher.doFinal(data, blocks * blockSize, lastBlockSize, rtn, blocks * outputBlockSize);
			}
			return RexseeSecurity.bytes2hex(rtn);
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + "Encryption:" + e.getLocalizedMessage();
		}
	}
	public String decryptWithRSA(String dataString, String key, boolean isPublicKey) {
		if (dataString == null || dataString.length() == 0) return RexseeApplication.EXCEPTION_PREFIX + "Data Error.";
		if (key == null || key.equals("")) return RexseeApplication.EXCEPTION_PREFIX + "Key Error.";
		byte[] data = RexseeSecurity.hex2bytes(dataString);
		try {
			Key theKey = (isPublicKey) ? string2publicKey_(RexseeSecurity.ALGORITHM_RSA, key) : string2privateKey_(RexseeSecurity.ALGORITHM_RSA, key);
			Cipher cipher = Cipher.getInstance(RexseeSecurity.ALGORITHM_RSA);//RSA/ECB/PKCS1Padding
			cipher.init(Cipher.DECRYPT_MODE, theKey);
			byte[] rtn;
			int blockSize = cipher.getBlockSize();
			int blocks = data.length / blockSize;
			ByteArrayOutputStream decodedStream = new ByteArrayOutputStream(data.length);
			for (int i = 0; i < blocks; i++) {
				decodedStream.write(cipher.doFinal(data, i * blockSize, blockSize));
			}
			rtn = decodedStream.toByteArray();
			return new String(rtn, mBrowser.application.getDefaultEncoding());
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + "Encryption:" + e.getLocalizedMessage();
		}
	}

	public boolean encryptWithRSA(final String sourcePath, final String targetPath, final String key, final boolean isPublicKey, boolean showProgress, boolean cancelable) {
		if (key == null || key.equals("")) {
			mBrowser.exception(getInterfaceName(), "Error RSA key");
			return false;
		}
		if (!isValidPath(sourcePath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		if (!isValidPath(targetPath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		BooleanRunnable runnable = new BooleanRunnable() {
			@Override
			public boolean run() {
				try {
					byte[] data = Utilities.getContent(sourcePath);
					if (data == null) {
						mBrowser.exception(getInterfaceName(), "Error on reading source file.");
						return false;
					}
					File targetFile = Utilities.prepareWriteFile(targetPath);
					if (targetFile == null) {
						mBrowser.exception(getInterfaceName(), "Error on prepare target file.");
						return false;
					}
					BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(targetFile));
					Key theKey = (isPublicKey) ? string2publicKey_(RexseeSecurity.ALGORITHM_RSA, key) : string2privateKey_(RexseeSecurity.ALGORITHM_RSA, key);
					Cipher cipher = Cipher.getInstance(RexseeSecurity.ALGORITHM_RSA);//RSA/ECB/PKCS1Padding
					cipher.init(Cipher.ENCRYPT_MODE, theKey);
					int blockSize = cipher.getBlockSize();
					byte[][] temp = RexseeSecurity.bytesSplit(data, blockSize);
					for (int i = 0; i < temp.length; i++) {
						bos.write(cipher.doFinal(temp[i]));
					}
					bos.close();
					return true;
				} catch (Exception e) {
					mBrowser.exception(getInterfaceName(), e);
					return false;
				}
			}
		};
		if (showProgress) {
			return new SyncTask().run(runnable, mBrowser, cancelable);
		} else {
			return new SyncTask().run(runnable, null, false);
		}
	}
	public boolean decryptWithRSA(final String sourcePath, final String targetPath, final String key, final boolean isPublicKey, boolean showProgress, boolean cancelable) {
		if (key == null || key.equals("")) {
			mBrowser.exception(getInterfaceName(), "Error RSA key.");
			return false;
		}
		if (!isValidPath(sourcePath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		if (!isValidPath(targetPath)) {
			mBrowser.exception(getInterfaceName(), "Invalid path.");
			return false;
		}
		BooleanRunnable runnable = new BooleanRunnable() {
			@Override
			public boolean run() {
				try {
					byte[] data = Utilities.getContent(sourcePath);
					if (data == null) {
						mBrowser.exception(getInterfaceName(), "Error on reading source file.");
						return false;
					}
					File targetFile = Utilities.prepareWriteFile(targetPath);
					if (targetFile == null) {
						mBrowser.exception(getInterfaceName(), "Error on prepare target file.");
						return false;
					}
					BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(targetFile));
					Key theKey = (isPublicKey) ? string2publicKey_(RexseeSecurity.ALGORITHM_RSA, key) : string2privateKey_(RexseeSecurity.ALGORITHM_RSA, key);
					Cipher cipher = Cipher.getInstance(RexseeSecurity.ALGORITHM_RSA);//RSA/ECB/PKCS1Padding
					cipher.init(Cipher.DECRYPT_MODE, theKey);
					int blockSize = cipher.getBlockSize();
					byte[][] temp = RexseeSecurity.bytesSplit(data, blockSize);
					for (int i = 0; i < temp.length; i++) {
						bos.write(cipher.doFinal(temp[i]));
					}
					bos.close();
					return true;
				} catch (Exception e) {
					mBrowser.exception(getInterfaceName(), e);
					return false;
				}
			}
		};
		if (showProgress) {
			return new SyncTask().run(runnable, mBrowser, cancelable);
		} else {
			return new SyncTask().run(runnable, null, false);
		}
	}

	//PBE
	public String generatePBESalt(int saleSize) {
		byte[] salt = new byte[saleSize];
		Random random = new Random();
		random.nextBytes(salt);
		return RexseeSecurity.bytes2hex(salt);
	}
	public String generatePBESalt(long seed, int saleSize) {
		byte[] salt = new byte[saleSize];
		Random random = new Random(seed);
		random.nextBytes(salt);
		return RexseeSecurity.bytes2hex(salt);
	}
	public String encryptWithPBE(String algorithm, String data, String key, String salt) {
		try {
			byte[] rtn = pbe_(algorithm, Cipher.ENCRYPT_MODE, data.getBytes(mBrowser.application.getDefaultEncoding()), key, RexseeSecurity.hex2bytes(salt));
			return RexseeSecurity.bytes2hex(rtn);
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + e.getLocalizedMessage();
		}
	}
	public boolean encryptWithPBE(String algorithm, String sourcePath, String targetPath, String key, String salt, boolean showProgress, boolean cancelable) {
		return pbeFile_(algorithm, Cipher.ENCRYPT_MODE, sourcePath, targetPath, key, salt, showProgress, cancelable);
	}
	public String decryptWithPBE(String algorithm, String data, String key, String salt) {
		try {
			byte[] rtn = pbe_(algorithm, Cipher.DECRYPT_MODE, RexseeSecurity.hex2bytes(data), key, RexseeSecurity.hex2bytes(salt));
			return new String(rtn, mBrowser.application.getDefaultEncoding());
		} catch (Exception e) {
			return RexseeApplication.EXCEPTION_PREFIX + e.getLocalizedMessage();
		}
	}
	public boolean decryptWithPBE(String algorithm, String sourcePath, String targetPath, String key, String salt, boolean showProgress, boolean cancelable) {
		return pbeFile_(algorithm, Cipher.DECRYPT_MODE, sourcePath, targetPath, key, salt, showProgress, cancelable);
	}

}
